feat: rebalance associative bitwise/Add/Multiply chains to avoid protobuf recursion limit#4588
feat: rebalance associative bitwise/Add/Multiply chains to avoid protobuf recursion limit#4588schenksj wants to merge 4 commits into
Conversation
) Follow-up to apache#4531 (deep And/Or chains). Protobuf's recursion limit (100) applies to any deeply nested BinaryExpr, so a long left-deep chain of other associative operators overflows the same way when the serialized plan is re-parsed. Extend the rebalancing (flattenAssociative + a balanced O(log n)-depth tree) to: - BitwiseAnd / BitwiseOr / BitwiseXor: always integral and exactly associative, so they reuse the existing createBalancedBinaryExpr directly. - Add / Multiply: gated via isAssociativeAndRebalanceable to integral types in LEGACY (wrapping, modular) eval mode -- the only exactly-associative case. Float isn't associative (Spark's ReorderAssociativeOperator excludes it too); ANSI/TRY make integer overflow position (which the grouping changes) observable; decimal precision grows per op. Those keep the existing left-deep serialization. Add and Multiply emit a MathExpr (eval_mode + return_type) rather than a BinaryExpr, so a new createBalancedMathExpr builds the balanced tree with the chain's uniform type and eval mode at every inner node. Tests mirror apache#4531: project 200-deep chains and assert Comet runs them natively with results matching Spark (which also verifies the associativity guarantee). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…associative-binary # Conflicts: # spark/src/main/scala/org/apache/comet/serde/arithmetic.scala # spark/src/test/scala/org/apache/comet/CometExpressionSuite.scala
|
Nice extension of the #4531 rebalancing. I read through the compatibility reasoning carefully and it holds up. A few notes and questions below. CompatibilityThe load-bearing claim is that serializing every inner node of the balanced tree with a single The integral + Questions / suggestions
Overall this looks correct and tightly scoped. The multiply test is a nice touch since 200 terms genuinely overflow and wrap, exercising the associativity guarantee rather than staying in a safe range. |
Addresses review feedback on apache#4588: note that the col + lit leaves take the non-rebalanced serde but stay small so nothing throws -- the bitwise chain is what's under test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the careful read, @andygrove — all three addressed: 1. ANSI / float / decimal deep chains. Added a "Not addressed (by design)" section to the description. On the fallback question, to be precise rather than reassuring: these don't degrade gracefully — they retain the exact pre-PR behavior. Serde still succeeds (it just emits the left-deep proto), so there's no plan-time fallback trigger, and Comet doesn't raise 2. Deep-bitwise test leaves. Added a comment noting the 3. Shared |
Closes #4577.
Follow-up to #4531 (deep And/Or chains). Protobuf's recursion limit (100) applies to
any deeply nested
BinaryExpr, so a long left-deep chain of other associative operatorsoverflows the same way when the serialized plan is re-parsed.
What
Extend the rebalancing (
flattenAssociative+ a balanced O(log n)-depth tree) to:they reuse the existing
createBalancedBinaryExprdirectly.isAssociativeAndRebalanceableto integral types inLEGACY (wrapping, modular) eval mode, the only exactly-associative case. Float isn't
associative (Spark's
ReorderAssociativeOperatorexcludes it too); ANSI/TRY makeinteger-overflow position observable and grouping changes it; decimal precision grows
per op. Those keep the existing left-deep serialization.
Add/Multiplyemit aMathExpr(eval_mode + return_type) rather than aBinaryExpr, so a newcreateBalancedMathExprbuilds the balanced tree with the chain's uniform type andeval mode at every inner node.
Tests
Mirror #4531: project 200-deep chains and assert Comet runs them natively with results
matching Spark (which also verifies the associativity guarantee).
Not addressed (by design)
Deep
Add/Multiplychains that are ANSI/TRY, floating-point, or decimal are notrebalanced — they aren't exactly associative, so regrouping could change the result or which
intermediate overflows. They keep the pre-existing left-deep serialization, so a sufficiently
deep such chain can still hit the protobuf recursion limit on re-parse, exactly as before this
PR. This is a deliberate correctness-over-depth trade-off, and strictly better than the prior
state (which had no rebalancing for any of these operators).